<?php if ( ! defined('BASEPATH')) exit('This file does not run directly. Sorry.'); ?>
<?php
/**
 * Kalaisoo.
 *
 * A lightweight no-framework framework written in PHP.
 *
 * @package Kalaisoo
 * @author $Author: info@sah-company.com $
 * @version $Id: redbean.php 145 2012-01-26 08:23:55Z info@sah-company.com $
 */

/**
 * Load third party "Redbean" vendor code.
 *
 * Redbean is a "sweet" PHP5 database ORM.
 *
 * @see http://redbeanphp.com
 */
require BASEPATH.'/vendor/RedBean3/rb'.EXT;

/**
 * Kalaisoo Model Class.
 *
 * This is our master Model class for RedBean.
 *
 * @see http://www.redbeanphp.com/manual/custom_mapping
 *
 * @package Kalaisoo
 * @subpackage Model
 * @author $Author: info@sah-company.com $
 */
abstract class Model extends RedBean_SimpleModel
{
	/**
	 * a controller can be injected here.
	 */
	protected $controller;
	
	/**
	 * an array with gestalt definitions.
	 *
	 * @var array
	 */
	protected $gestalt;
	
	/**
	 * stores the table names of this and collected beans.
	 *
	 * @var array
	 */
	protected $tables;
	
	/**
	 * stores the column names of this and collected beans.
	 *
	 * @var array
	 */
	protected $fields;
	
	/**
	 * stores the joins of this and collected beans.
	 *
	 * @var array
	 */
	protected $joins;

	/**
	 * name of a property to use as a sort key.
	 *
	 * @var string
	 */
	public static $sortField;
	
	/**
	 * either 'asc' or 'desc' to describe the sort direction.
	 *
	 * @var string
	 */
	public static $sortDirection;
	
	/**
	 * errors after validation or other actions taken on the model.
	 *
	 * @var array
	 */
	protected $error = array();
	
	/**
	 * the default filter to apply
	 *
	 * @var string
	 */
	public $filter = '';
	
	/**
	 * filters for this model
	 *
	 * @var array
	 */
	public $filters = array();
	
	/**
	 * list of properties to use as automatic tags
	 *
	 * @var array
	 */
	public $autoTags = array(
	);
	
	/**
	 * returns either 1 or -1 as the result of comparing the two parameters.
	 *
	 * @param string $a The first comparison parameter
	 * @param string $b The second comparison parameter
	 * @return int
	 */
	public static function sortString($a, $b) {
		$sortField = self::$sortField;
		$sortDirection = strtolower(self::$sortDirection);
		if ($sortDirection == 'asc') {
			return strcasecmp($a->$sortField, $b->$sortField);
		} else {
			return -strcasecmp($a->$sortField, $b->$sortField);
		}
	}
	
	/**
	 * returns either 1 or -1 as the result of comparing the two numbers.
	 *
	 * @param mixed The first comparion parameter
	 * @param mixed The second comparion parameter
	 * @return int
	 */
	public static function sort($a, $b)
	{
		$sortField = self::$sortField;
		$sortDirection = strtolower(self::$sortDirection);
		if ($a->$sortField == $b->$sortField) {
			return 0;
		}
		if ($sortDirection == 'asc') {
			return ($a->$sortField > $b->$sortField) ? +1 : -1;
		} else {
			return ($a->$sortField < $b->$sortField) ? +1 : -1;
		}
	}
	
	/**
	 * sets the controller for this model.
	 *
	 * @param Controller $controller
	 */
	public function setController(Controller $controller)
	{
		$this->controller = $controller;
	}
	
	/**
	 * return controller object.
	 *
	 * @return Controller
	 */
	public function controller()
	{
		return $this->controller;
	}
	
	/**
	 * adds an error string token to the error stack or set a key with value token in errors.
	 *
	 * @param string $token The error token to add
	 * @param string $key If given this value is used as key for the token
	 * @return void
	 */
	public function addError($token, $key = null)
	{
		if ($key === null) {
			$this->error[] = $token;
		}
		else {
			$this->error[$key] = $token;
		}
	}
	
	/**
	 * returns the array with errors.
	 *
	 * @return array
	 */
	public function error()
	{
		return $this->error;
	}
	
	/**
	 * returns a view of a single bean.
	 *
	 * If the optional parameter is set a view with this name is retrieved, default 'shortinfo'.
	 * The second parameter lets you pass an array to the view. Anyway the bean of the model is
	 * passed in array key 'bean' anyway.
	 *
	 * @param string $view, defaults to shortinfo
	 * @param array (optional) $data
	 * @return string
	 */
	public function view($view = 'shortinfo', array $data = array())
	{
		$template = new Template();
		$templateName = sprintf('%s/%s', $this->bean->getMeta('type'), $view);
		$data['bean'] = $this->bean; // push this bean into the view
		return $template->view($templateName, $data, true);
	}
	
	/**
	 * returns meta information from a entry bean.
	 *
	 * @param mixed $entry
	 * @return string
	 */
	public function info($entry)
	{
		if ( ! $this->bean->getId() || ! $entry->getId()) return __('core_entry_info_template_blank');
		if ($entry->revision == 1) return sprintf(__('core_entry_info_template_virgin'), $entry->creation_date, $entry->fetchAs('user')->creation_user->name());
		return sprintf(__('core_entry_info_template'), $entry->creation_date, $entry->fetchAs('user')->creation_user->name(), $entry->modification_date, $entry->fetchAs('user')->modification_user->name(), $entry->revision);
	}
	
	/**
	 * returns the automatic tags for this bean.
	 *
	 * This will call gestalt('autoTag') and when there is a list of tags a model attribute
	 * is added to the array of autoTags which will be returned as an result.
	 *
	 * @return array
	 */
	public function autoTags()
	{
		$tags = array();
		foreach ($this->getTags() as $property) {
			if ($tag = trim($property)) {
				$tags = array_merge($tags, $this->makeTags($tag));
			}
		}
		return $tags;
	}
	
	/**
	 * splits the words in $text into an array using a neat regex.
	 *
	 * I found this regex on the web, but i can not remember where.
	 *
	 * @param string $text
	 * @return array
	 */
	protected function makeTags($text)
	{
		return preg_split('/((^\p{P}+)|(\p{P}*\s+\p{P}*)|(\p{P}+$))/', $text, -1, PREG_SPLIT_NO_EMPTY);
	}
	
	/**
	 * returns an array with special actions (besides curd) that the model can take.
	 *
	 * The curd (create, update, read, delete) actions are handled by the Editor
	 * controller. But a model can have many special methods. This function has to
	 * return an array with all these additional actions. You have to reimplement this
	 * method in your own model class.
	 *
	 * @see Model_Todo::complete()
	 *
	 * @return array
	 */
	public function actions()
	{
		return array();
	}
	
	/**
	 * returns an array with special filters after which the model can be filtered.
	 *
	 * @see Editor::grid()
	 *
	 * @return array
	 */
	public function filters()
	{
		return $this->filters;
	}
	
	/**
	 * returns the named filter or if no parameter was given the default filter
	 *
	 * @param string $name
	 * @return array
	 */
	public function filter($name = '')
	{
		if ( ! $filters = $this->filters()) return '';
		if (empty($name)) $name = $this->filter;
		if ( ! isset($filters[$name])) return '';
		return $filters[$name];
	}
	
	/**
	 * returns the default filter token name
	 *
	 * @return string
	 */
	public function filterToken()
	{
		return $this->filter;
	}
	
	/**
	 * analyzes the bean type and sets tables, fields and joins arrays.
	 *
	 * If you give a second parameter $makeView, a mysql view for this will be created.
	 *
	 * @param string $type
	 * @param mixed (optional) $makeView
	 * @return bool
	 */
	public function describe($type = '', $makeView = null)
	{
		$bean = R::dispense($type);
		$this->tables = array();
		$this->fields = array();
		$this->joins = array();
		$this->describeWorkhorse($bean);
		if ($makeView) {
			return $this->makeView($bean, $makeView);
		}
		return true;
	}
	
	/**
	 * takes the tables, fields and joins to create or replace a mysql view.
	 *
	 * @param mixed $bean
	 * @param string $viewName
	 * @return bool
	 */
	protected function makeView($bean, $viewName)
	{
		$tables = implode(', ', $this->tables());
		$joins = implode(' AND ', $this->joins());
		$fields = glue($this->fields(), ' AS ', '', ' ', ', ');
		if ( ! empty($joins)) {
			$sql_cmd = "CREATE OR REPLACE VIEW {$viewName} AS ( SELECT %s FROM %s WHERE %s )";
			$sql = sprintf($sql_cmd, $fields, $tables, $joins);
		} else {
			$sql_cmd = "CREATE OR REPLACE VIEW {$viewName} AS ( SELECT %s FROM %s)";
			$sql = sprintf($sql_cmd, $fields, $tables);
		}
		try {
			R::exec($sql);
			return true;
		} catch (Exception $e) {
			error_log('Could not create a view for '.$bean->getMeta('type'));
			error_log($sql);
			return false;
		}
	}
	
	/**
	 * returns the default order clause for sql queries
	 *
	 * @return mixed
	 */
	public function orderClause()
	{
		return '';
	}
	
	/**
	 * returns true after setting tables, fields and joins of the given bean.
	 *
	 * @param mixed $bean
	 * @return bool
	 */
	public function describeWorkhorse($bean)
	{
		Registry::instance()->loader->model($bean->getMeta('type'), true);
		$gestalt = $bean->gestalt();
		$this->tables[$bean->getMeta('type')] = $bean->getMeta('type');
		$this->fields[$bean->getMeta('type').'.id'] = $bean->getMeta('type').'_id';
		foreach ($gestalt as $n=>$feature) {
			if (in_array($feature['type'], array('own', 'shared', 'related', 'nested'))) {
				//$this->tables[] = $feature['name'];
				if (isset($feature['parameters']['alias'])) {
					$realName = $feature['parameters']['alias'];
				} else {
					$realName = $feature['name'];
				}
				Registry::instance()->loader->model($realName, true);
				$subBean = R::dispense($realName);
				$this->describeWorkhorse($subBean);
				if ($feature['type'] == 'own') {
					$this->joins[$realName] = $realName.'.'.$bean->getMeta('type').'_id = '.$bean->getMeta('type').'.id';
				} elseif ($feature['type'] == 'nested') {
					$this->joins[$realName] = $realName.'.id = '.$bean->getMeta('type').'.'.$feature['name'].'_id';
				}
			} else {
				$this->fields[$bean->getMeta('type').'.'.$feature['name']] = $bean->getMeta('type').'_'.$feature['name'];
			}
		}
		return true;
	}
	
	/**
	 * returns array with table names.
	 *
	 * @return array
	 */
	public function tables()
	{
		return array_values($this->tables);
	}
	
	/**
	 * returns array with column names.
	 *
	 * @return array
	 */
	public function fields()
	{
		return $this->fields;
	}
	
	/**
	 * returns array with join names.
	 *
	 * @return array
	 */
	public function joins()
	{
		return array_values($this->joins);
	}
	
	/**
	 * set the gestalt description array of a bean from which can be used with Gestalt.
	 */
	abstract public function loadGestalten();
	
	/**
	 * returns the tags of a bean.
	 *
	 * @return array
	 */
	abstract public function getTags();
	
	/**
	 * returns the gestalt of a bean from which a form can be made.
	 *
	 * This will return a array to be used with a form builder. The optional parameter
	 * lets you select different gestalt descriptions.
	 *
	 * @param string $name
	 * @return array
	 */
	public function gestalt($name = 'default') {
		if ( ! isset($this->gestalt[$name])) $this->loadGestalten();
		if ( ! isset($this->gestalt[$name])) return array(); // not defined
		return $this->gestalt[$name];
	}
	
	/**
	 * validates the bean and returns wether the bean was valid or not.
	 *
	 * When validating errors may be set in the error property.
	 *
	 * @return bool
	 */
	abstract public function validate();
}

/**
 * Load our database configuration from the registry.
 */
$config = Registry::instance()->config['db'];

/**
 * Setup redbean with our database parameters.
 *
 * If you are using other databases, please use R::addDatabase and R::useDatabase
 * respectively or otherwise your actions take place in this database.
 */
R::setup("{$config['driver']}:host={$config['host']};dbname={$config['database']}", $config['username'], $config['password']);

/**
 * If constant MODUS does not exist or is not equal DEVELOPMENT, RedBean shall freeze.
 */
if ( ! defined('MODUS') || MODUS !== 'DEVELOPMENT') {
	R::freeze(true);
}
?>